# -*- coding: utf-8 -*-


import gurobipy as gr
import pandas as pd
import numpy as np

class ServerEdgeTest:
    
    """
    class to test the load balance function of different distribution result
    """
    
    def __init__(self):
        pass
    
    def load_distribution2(self, lanti_1, long_1, lanti_2, long_2, load):
        """find the locations in the loca2 that are emerged in the loca1 and store the index of the opposite one
        
        param
        -----------
            lanti_1 : list
                store all the lantitude information of the loca1
            lanti_2 : list
                store all the lantitude information of the loca2
            long_1 : list
                store all the longtitude information of the loca1
            long_2 : list
                store all the longtitude information of the loca2
            load : list
                store all the load information of the loca2
            
        return
        ----------
            load_rebuild : list
                store the load in both loca1 and loca2, others are 0
            num : int
                the number of the loca included in both loca1 and loca2
            new_addition_index : list
                store the index in loca2 that are not emerged in the loca1
        
        refer
        ----------
            Other func : None
        """
        base_num = len(lanti_1)
        num = 0
        new_addition_index = []
        load_rebuild = [0 for i in range(base_num)]
        # find the loca in loca2 that emerge in loca1
        for i in range(len(lanti_2)):
            try:
                lanti_index = lanti_1.index(lanti_2[i])
            except:
                new_addition_index.append(i)
                lanti_index = -1
                
            try:
                long_index = long_1.index(long_2[i])
            except:
                long_index = -2
                
            if lanti_index == long_index:
                load_rebuild[lanti_index] = load[i]
                num = num + 1
                
        return load_rebuild, num, new_addition_index
    
    def array_calculate_distance(self, location_a, location_b):
        """calculate the actual distance between the element in loca_a and loca_b
        
        param
        ----------
            location_a : list
                store all the location information in first location_list
            location_b :list
                store all the location information in second location_list
               
        return
        ----------
            12742 * np.arcsin(np.sqrt(a)) : list
                store all the distance information between the elements in the two lists
        
        refer
        ----------
            Other func : None
        """
        p = 0.017453292519943295
        
        a = (0.5 - np.cos((location_b[:, 0] - location_a[:, 0]) * p) / 2 + np.cos(location_a[:, 0] * p) 
        * np.cos(location_b[:, 0] * p) * (1 - np.cos((location_b[:, 1] - location_a[:, 1]) * p)) / 2)
        return 12742 * np.arcsin(np.sqrt(a)) 
   
    def location_divide(self, location_array, k_near):
        """find the near k base_station and get their index
        
        param
        ----------
            location_array : array
                the array to store both the lantitude and longtitude information of the location
                structure : nx2
                content : first column ---> lantitude , second column ---> longitude
            k_near : int
                the number of the near base station we will find
        
        return
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
        
        refer
        ----------
            Other func :  self.array_calculate_distance
        """
        base_num = len(location_array)
        list_near = list()
        list_far = list()
        list_assign = []
        
        for i in range(base_num):
            list_assign.append([])
            
        for index in range(base_num):
            base_index = location_array[index]
            base_all = np.ones((base_num,1)) * base_index
            base_distance =  self.array_calculate_distance(base_all, location_array)
            distance_sort_index = np.argsort(base_distance)
            distance_near = list(distance_sort_index[0 : k_near])
            distance_far = list(distance_sort_index[k_near : base_num])
            list_near.append(distance_near)
            list_far.append(distance_far)
            for i in range(k_near):
                list_assign[distance_near[i]].append(index)
                
        return list_near, list_far, list_assign
   
    def ilp_placement(self, list_near, list_far, list_assign, k_place, workload, y):
        """we use the ilp model to test load balance function of each formulation
        
        param
        ----------
            list_near : list
                store the k near base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the k near base stations
            list_far : list
                store the other base stations of each base
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the index of the other base stations
            list_assign : list
                store the index of which station provide the flow to one base station
                structure : [[content1],[content2],.......,[contentn]]
                content : [] ---> the indexes of base stations that share the flows to one base station according to the index
            k_place : int
                The number of the servers we want to assign
            workload : list
                Store the list of the workload about each base
            y : list
                Store the distribution of the servers in each base station
        return
        ----------
            u_list : array
                store the proportion of the assignment in each base station
            final_optimal : 1/beta 
                parameter to describe the load balance function

        refer
        ----------
            Other func :  None
            Document : The introduction document of gurobi
        """
        base_num = len(list_near)
        list_var = []
        
        model_placement = gr.Model('Lip_placement')
        u = model_placement.addVars(base_num, len(list_near[0]), vtype='S', lb=0)
        a = model_placement.addVar(vtype='S', lb=0)
        
        model_placement.setObjective(a, gr.GRB.MAXIMIZE)
        model_placement.addConstrs(
                sum(u[i,j] 
                for j in range(len(list_near[0]))) == a 
                for i in range(base_num)
        )
        model_placement.addConstrs(
                sum(u[list_assign[i][j], list_near[list_assign[i][j]].index(i)] * workload[list_assign[i][j]]
                for j in range(len(list_assign[i]))) <= y[i] 
                for i in range(base_num)
        )
        model_placement.optimize()
        
        if model_placement.Status == gr.GRB.OPTIMAL:
            for var in model_placement.getVars():
                list_var.append(var.x)
            u_list = np.array(list_var[0 : base_num*len(list_near[0])])
            u_list = u_list.reshape((base_num, len(list_near[0])))
            final_optimal = list_var[base_num * len(list_near[0])]
        return u_list, final_optimal
    
    def final_target(self, path, name, lanti1, long1, num, range_distance):
        """Get the load balance function in certain distance assigned
        
        param
        ----------
            path : string
                The path to the test data
            name : name
                The name of test data
            lanti1 :  list
                The lanti list of the base station that have been assigned servers
            long1 : list
                The long list of the base station that have been assigned servers
            num : int
                The server number
            range_distance : int
                The number of the base station included to be the near k during the test process
            
        return
        ----------
            final_optimal : float
                The load balance parameter
              
         refer
        ----------
            Other func :  load_distribution2, location_divide, ilp_placement
        """
        tsv_information = pd.read_csv(path + name)
        lanti2_infor = tsv_information['lanti']
        lanti2_list = lanti2_infor.tolist()
        lanti2_list = list(np.round(lanti2_list, 6))
        long2_infor = tsv_information['long']
        long2_list = long2_infor.tolist()
        long2_list = list(np.round(long2_list, 6))
        fre_infor = tsv_information['frequ']
        fre_list = fre_infor.tolist()
        new_lanti1 = lanti1
        new_long1 = long1
        new_num = num
        load_rebuild, num0, new_addition_index = self.load_distribution2(lanti1, long1, lanti2_list, long2_list, fre_list)
        new_load_rebuild = load_rebuild
        
        for i in range(len(new_addition_index)):
            new_lanti1.append(lanti2_list[i])
            new_long1.append(long2_list[i])
            new_num.append(0)
            new_load_rebuild.append(fre_list[i])
        
        loca_array = np.zeros((len(new_lanti1), 2))
        loca_array[:, 0] = np.array(new_lanti1)
        loca_array[:, 1] = np.array(new_long1)
        distance_near, distance_far, list_assign = self.location_divide(loca_array, range_distance)
        u_list, final_optimal = self.ilp_placement(distance_near, distance_far, list_assign, 400, load_rebuild, new_num)
        return final_optimal
    
    def target_list(self, path, lanti1, long1, num, range_distance):
        """
        Get the load balance in different part of time
        """
        name_list = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11']
        target_list = []
        
        for i in name_list:
            name = '/6_1 ' + i + '_frequ.csv'
            final_optimal = self.final_target(path, name, lanti1, long1, num, range_distance)
            target_list.append(final_optimal)
            print('This time have been calculated/n')
            
        return target_list
    
    def range_target(self, path, name, lanti1, long1, num, distance):
        """
        Get the load balance function in different distances
        """
        name_total = '/6_7 ' + name + '_frequ.csv'
        final_optimal = self.final_target(path, name_total, lanti1, long1, num, distance)
        return final_optimal
    
    def sum_range_target(self, path, name, lanti1, long1, num):
        """
        Get the total load balance function in all distances
        """
        sum_optimal = []
        for i in range(30, 201, 10):
            final_optimal = self.range_target(path, name, lanti1, long1, num, i)
            sum_optimal.append(final_optimal)
        return sum_optimal
    